package cn.wolfcode.service.impl;

import cn.wolfcode.common.domain.UserInfo;
import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.web.CommonCodeMsg;
import cn.wolfcode.common.web.Result;
import cn.wolfcode.domain.*;
import cn.wolfcode.feign.AlipayFeignService;
import cn.wolfcode.feign.IntegralFeignService;
import cn.wolfcode.mapper.OrderInfoMapper;
import cn.wolfcode.mapper.PayLogMapper;
import cn.wolfcode.mapper.RefundLogMapper;
import cn.wolfcode.mq.MQConstant;
import cn.wolfcode.mq.OrderTimeout;
import cn.wolfcode.redis.SeckillRedisKey;
import cn.wolfcode.service.IOrderInfoService;
import cn.wolfcode.service.ISeckillProductService;
import cn.wolfcode.util.IdGenerateUtil;
import cn.wolfcode.vo.PayResult;
import cn.wolfcode.web.msg.SeckillCodeMsg;
import com.alibaba.fastjson.JSON;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;

/**
 * Created by wolfcode
 */
@Slf4j
@Service
public class OrderInfoSeviceImpl implements IOrderInfoService {
    @Autowired
    private ISeckillProductService seckillProductService;
    @Autowired
    private OrderInfoMapper orderInfoMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private PayLogMapper payLogMapper;
    @Autowired
    private RefundLogMapper refundLogMapper;
    @Autowired
    private AlipayFeignService alipayFeignService;
    @Autowired
    private IntegralFeignService integralFeignService;

    @Override
    public OrderInfo selectByUserIdAndSeckillId(Long userId, Long seckillId, Integer time) {
        return orderInfoMapper.selectByUserIdAndSeckillId(userId, seckillId, time);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public String doSeckill(UserInfo userInfo, SeckillProductVo vo) {
        // 1. 扣除秒杀商品库存
        seckillProductService.decrStockCount(vo.getId(), vo.getTime());
        // 2. 创建秒杀订单并保存
        OrderInfo orderInfo = this.buildOrderInfo(userInfo, vo);
        orderInfoMapper.insert(orderInfo);
        return orderInfo.getOrderNo();
    }

    @Override
    public void createOrderFallbackRedis(Long seckillId, Integer time, Long userPhone) {
        log.info("[订单回补] 创建订单失败, 回补 Redis 数据：seckillId={}, time={}, phone={}", seckillId, time, userPhone);
        // 1. 用户下单标识(如果当前标记大于最大值, 结果=最大值-1, 否则就当前值-1)
        this.fallbackUserOrderFlag(seckillId, userPhone);
        // 2. 令牌桶（库存）(如果当前值<1, 就直接设置结果=1, 否则 = 当前值+1)
        this.fallbackStockCount(seckillId, time);
        // 3. 本地标识(发送 mq 广播消息, 将本地标识设置为false, 或直接删除)
        rocketMQTemplate.syncSend(MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC, seckillId);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkOrderPayTimeout(OrderTimeout message) {
        // 1. 查询订单状态, 判断是否为已支付, 如果已支付就不管了
        // 2. 如果是未支付
        // 3. 修改订单状态为超时未支付
        int row = orderInfoMapper.updateCancelStatus(message.getOrderNo(), OrderInfo.STATUS_TIMEOUT);
        if (row > 0) {
            // 4. 回补 MySQL 库存
            seckillProductService.incrStockCount(message.getSeckillId());
            // 5. 回补 Redis 中的数据
            this.createOrderFallbackRedis(message.getSeckillId(), message.getTime(), message.getUserPhone());
        }
    }

    @Override
    public OrderInfo findById(String orderNo) {
        return orderInfoMapper.find(orderNo);
    }

    // @CacheEvict(key = "'orders:'+#result.outTradeNo", cacheNames = "seckill")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void paySuccess(PayResult result) {
        // 1. 校验订单编号是否正确
        OrderInfo orderInfo = this.findById(result.getOutTradeNo());
        if (orderInfo == null) {
            throw new BusinessException(SeckillCodeMsg.REMOTE_DATA_ERROR);
        }
        // 2. 校验交易金额是否正确
        if (!orderInfo.getSeckillPrice().toString().equals(result.getAmount())) {
            throw new BusinessException(SeckillCodeMsg.REMOTE_DATA_ERROR);
        }
        // 3. 判断订单状态是否正确
        if (!OrderInfo.STATUS_ARREARAGE.equals(orderInfo.getStatus())) {
            // 只是保证幂等性, 不需要抛出异常
            log.warn("[支付回调] 订单状态异常, 执行幂等逻辑: {}", JSON.toJSONString(orderInfo));
            return;
        }
        // 4. 修改订单状态为已支付
        int row = orderInfoMapper.changePayStatus(orderInfo.getOrderNo(), OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_ONLINE);
        if (row <= 0) {
            // 只是保证幂等性, 不需要抛出异常
            log.warn("[支付回调] 订单状态异常, 修改订单状态失败: {}", JSON.toJSONString(orderInfo));
            return;
        }
        // 5. 保存支付流水记录
        insertPayLog(result.getOutTradeNo(), result.getTradeNo(), result.getAmount(), OrderInfo.PAY_TYPE_ONLINE);
    }

    private void insertPayLog(String outTradeNo, String tradeNo, String amount, Integer type) {
        PayLog payLog = new PayLog();
        payLog.setPayType(type);
        payLog.setNotifyTime(System.currentTimeMillis() + "");
        payLog.setTradeNo(tradeNo);
        payLog.setTotalAmount(amount);
        payLog.setOutTradeNo(outTradeNo);
        payLogMapper.insert(payLog);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void doRefund(String orderNo, Long phone) {
        // 1. 基于订单编号查询订单对象
        OrderInfo orderInfo = findById(orderNo);
        log.info("[退款] 发起退款: {}", JSON.toJSONString(orderInfo));
        // 2. 判断用户是否是下单用户
        if (!phone.equals(orderInfo.getUserId())) {
            throw new BusinessException(CommonCodeMsg.ILLEGAL_OPERATION);
        }
        // 3. 判断订单状态是否为已支付
        if (!OrderInfo.STATUS_ACCOUNT_PAID.equals(orderInfo.getStatus())) {
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }

        String reason = "不想要了";

        // 6. 判断支付类型, 根据支付类型进行退款
        Result<Boolean> result = null;
        if (OrderInfo.PAY_TYPE_ONLINE == orderInfo.getPayType()) {
            // 7. 远程调用支付宝退款
            RefundVo vo = new RefundVo(orderNo, orderInfo.getSeckillPrice().toString(), reason);
            result = alipayFeignService.refund(vo);

            if (!result.getData(true)) {
                throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
            }

            this.commitRefund(orderInfo);
            return;
        }

        OperateIntergralVo vo = new OperateIntergralVo();
        vo.setInfo(reason);
        vo.setPk(orderNo);
        vo.setValue(orderInfo.getIntergral());
        vo.setUserId(phone);
        log.info("[积分退款] 发送本地事务消息：{}", JSON.toJSONString(vo));
        // 7. 发送事务消息，通知远程增加积分
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(MQConstant.INTEGRAL_REFUND_TX_GROUP,
                MQConstant.INTEGRAL_REFUND_TOPIC,
                MessageBuilder.withPayload(vo).setHeader("orderNo", orderNo).build(),
                orderNo);

        LocalTransactionState transactionState = transactionSendResult.getLocalTransactionState();
        log.info("[积分退款] 本地事务消息已发送：sendStatus={}, transactionState={}", transactionSendResult.getSendStatus(), transactionState);

        if (!transactionSendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void commitRefund(String orderNo) {
        OrderInfo orderInfo = findById(orderNo);
        this.commitRefund(orderInfo);
    }

    private void commitRefund(OrderInfo orderInfo) {
        // 4. 先基于乐观锁尝试修改订单状态为已退款
        int row = orderInfoMapper.changeRefundStatus(orderInfo.getOrderNo(), OrderInfo.STATUS_REFUND);
        if (row <= 0) {
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }

        // 5. 记录退款日志
        RefundLog refundLog = new RefundLog();
        refundLog.setOutTradeNo(orderInfo.getOrderNo());
        refundLog.setRefundType(orderInfo.getPayType());
        refundLog.setRefundTime(new Date());
        refundLog.setRefundReason("不想要了");
        refundLog.setRefundAmount(orderInfo.getSeckillPrice().toString());
        refundLogMapper.insert(refundLog);

        // 8. 回滚数据
        // 回补 MySQL 库存
        seckillProductService.incrStockCount(orderInfo.getSeckillId());
        // 回补 Redis 中的数据
        this.createOrderFallbackRedis(orderInfo.getSeckillId(), orderInfo.getSeckillTime(), orderInfo.getUserId());
    }

    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public void integralPay(String orderNo, Long phone) {
        // 1. 基于订单号查询订单对象
        OrderInfo orderInfo = findById(orderNo);
        // 2. 判断是否是当前用户下单
        if (!phone.equals(orderInfo.getUserId())) {
            throw new BusinessException(SeckillCodeMsg.REMOTE_DATA_ERROR);
        }
        // 3. 判断订单状态必须是未支付
        if (!OrderInfo.STATUS_ARREARAGE.equals(orderInfo.getStatus())) {
            throw new BusinessException(SeckillCodeMsg.PAY_STATUS_ERROR);
        }
        // 4. 扣除成功更新订单状态为已支付
        int row = orderInfoMapper.changePayStatus(orderNo, OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_INTERGRAL);
        if (row <= 0) {
            throw new BusinessException(SeckillCodeMsg.INTERGRAL_SERVER_ERROR);
        }

        OperateIntergralVo vo = new OperateIntergralVo();
        vo.setPk(orderNo);
        vo.setUserId(phone);
        vo.setValue(orderInfo.getIntergral());
        vo.setInfo("秒杀: " + orderInfo.getProductName());
        // 5. 调用积分支付扣除积分
        Result<String> result = integralFeignService.integralPay(vo);
        // 6. 判断是否扣除成功
        String tradeNo = result.getData(true);
        if (StringUtils.isEmpty(tradeNo)) {
            throw new BusinessException(SeckillCodeMsg.INTERGRAL_SERVER_ERROR);
        }

        // 7. 记录支付日志
        insertPayLog(orderNo, tradeNo, orderInfo.getIntergral() + "", OrderInfo.PAY_TYPE_INTERGRAL);
    }

    //@Cacheable(key = "'orders:' + #orderNo", cacheNames = "seckill")
    @Override
    public OrderInfo findByIdFromRedis(String orderNo) {
        String json = redisTemplate.opsForValue().get("seckill::orders:" + orderNo);
        if (StringUtils.isNotEmpty(json)) {
            return JSON.parseObject(json, OrderInfo.class);
        }
        return findById(orderNo);
    }

    private void fallbackStockCount(Long seckillId, Integer time) {
        String script =
                "local localKey = KEYS[1]\n" +
                        "local hashKey = ARGV[1]\n" +
                        "local value = tonumber(redis.call('HGET', localKey, hashKey))\n" +
                        "if value < 1 then\n" +
                        "\tredis.call('HSET', localKey, hashKey, 1)\n" +
                        "else\n" +
                        "\tredis.call('HSET', localKey, hashKey, value+1)\n" +
                        "end\n";
        log.info("[订单回补] LUA 回补令牌桶库存: {}", script);
        String localKey = SeckillRedisKey.SECKILL_REAL_COUNT_HASH.join(time + "");
        redisTemplate.execute(RedisScript.of(script), Collections.singletonList(localKey), seckillId + "");
    }

    private void fallbackUserOrderFlag(Long seckillId, Long userPhone) {
        String script =
                "local localKey = KEYS[1]\n" +
                        "local hashKey = KEYS[2]\n" +
                        "local max = tonumber(ARGV[1])\n" +
                        "local value = tonumber(redis.call('HGET', localKey, hashKey))\n" +
                        "if value > max then\n" +
                        "\tredis.call('HSET', localKey, hashKey, max-1)\n" +
                        "elseif value <= 0 then\n" +
                        "\tredis.call('HDEL', localKey, hashKey)\n" +
                        "else\n" +
                        "\tredis.call('HSET', localKey, hashKey, value-1)\n" +
                        "end\n";
        log.info("[订单回补] LUA 脚本回补用户下单标识: {}", script);
        String localKey = SeckillRedisKey.SECKILL_ORDER_HASH.join(seckillId + "");
        redisTemplate.execute(RedisScript.of(script), Arrays.asList(localKey, userPhone + ""), "2");
    }

    private OrderInfo buildOrderInfo(UserInfo userInfo, SeckillProductVo vo) {
        Date now = new Date();
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCreateDate(now);
        orderInfo.setDeliveryAddrId(1L);
        orderInfo.setIntergral(vo.getIntergral());
        // 归档
        orderInfo.setOrderNo(IdGenerateUtil.get().nextId() + "");
        orderInfo.setPayType(OrderInfo.PAY_TYPE_ONLINE);
        orderInfo.setProductCount(1);
        orderInfo.setProductId(vo.getProductId());
        orderInfo.setProductImg(vo.getProductImg());
        orderInfo.setProductName(vo.getProductName());
        orderInfo.setProductPrice(vo.getProductPrice());
        orderInfo.setSeckillDate(now);
        orderInfo.setSeckillId(vo.getId());
        orderInfo.setSeckillPrice(vo.getSeckillPrice());
        orderInfo.setSeckillTime(vo.getTime());
        orderInfo.setStatus(OrderInfo.STATUS_ARREARAGE);
        orderInfo.setUserId(userInfo.getPhone());
        return orderInfo;
    }
}
